iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 19
0

已經搞了好幾天的 ORM,今天總算要做個結尾啦,這個系列我們從 file_model.rb 用 JSON 格式檔案當作資料庫,在到 sqlite_model.rb 做了初步的 ORM 效果,中間我們也學習了各種動態生成 Attribute 的寫法,今天要來介紹另一個 ORM 很常用的技巧,就是 where

相信大家對於 where 應該不陌生,就像是下面範例一樣

Task.where(id: 1)

不過在進行 where 之前,我們一樣先把 ORM 其他功能都補齊吧!

因為有了前面的例子,基本的操作就比較快速帶過,先來加上 save

因為 save 是屬於 isntance method,所以我會做在 persistence.rb

# mavericks/lib/mavericks/data_record/persistence.rb

module Mavericks
  module DataRecord
    module Persistence
      def initialize(attributes = {})
        self.class.set_column_to_attribute
        @attributes = attributes
        
        # 用 new_record 來紀錄有沒有重覆儲存
        @new_record = true
      end

      def new_record?
        @new_record
      end

      def save!
        return true unless new_record?

        vals = @attributes.values.map { |value| self.class.to_sql(value) }
        self.class.connection.execute <<-SQL
          INSERT INTO #{self.class.table_name} (#{@attributes.keys.join(',')})
          VALUES (#{vals.join ","});
        SQL
        @new_record = false
      end

      def save
        self.save! rescue false
      end
    end
  end
end

會發現其實跟 sqlite_model.rb 很像,但我們多了一些處理,例如同一個物件,我們會用 @new_record 紀錄是不是重複 save,如果是的話就不會再讓他存進資料庫,其他部分都在 sqlite_model.rb 講解過,例如 savesave!

另外因為我們也還是沿用 to_sql 這個語法,所以我們還需要在 method 加上 to_sql

# mavericks/lib/mavericks/data_record/method.rb

def to_sql(val)
  case val
  when Numeric
    val.to_s
  when String
    "'#{val}'"
  else
    raise "Can't support #{val.class} to SQL!"
  end
end

有了 save,要怎麼知道有沒有存成功呢?還記得之前的 count 嗎?這裡原理一樣應該不難,只是要注意 count 也是 class method,要擺在 method 這個檔案裡面

# mavericks/lib/mavericks/data_record/method.rb

def count
  self.connection.execute(<<-SQL)[0]['count']
    SELECT COUNT(*) FROM #{self.table_name}
  SQL
end

回到 sqlite_test.rb 測試一下吧

# just_do/sqlite_test.rb

require 'mavericks/data_record'

Mavericks::DataRecord::Base.establish_connection

class Task < Mavericks::DataRecord::Base
end

task = Task.new(title: '鐵人30', content: '一天一篇文章')
task.save
puts Task.count

一樣每執行一次,count 的數量都會加 1,至於 save! 和 save 也試試看效果吧

# just_do/sqlite_test.rb

require 'mavericks/data_record'

Mavericks::DataRecord::Base.establish_connection

class Task < Mavericks::DataRecord::Base
end

# 故意取一個沒有的 column 名稱
task = Task.new(name: '鐵人30', content: '123')

puts task.save
# false

task.save!
# ERROR:  column "name" of relation "tasks" does not exist

之前的 ALL 裡面到底都包些什麼?

我們之前做得 all,裡面其實包的是 Hash,來假裝有 Attribute 這件事情

row.map do |attr|
  data = Hash[schema.keys.zip attr]
  self.new data
end

但在 Rails 裡面,裡面包的可是一個個物件,所以你是可以用這樣的方式來取得資料

Task.all.each do |task|
  taks.title
  # 鐵人30
end

那我們來改良一下 all吧!改良之前,我們先建立一個 class Relation,這也是我們今天主題要用到的東西,他其實就是紀錄了所有物件

# mavericks/lib/mavericks/data_record/relation.rb

module Mavericks
  class Relation
    def initialize(klass)
      @klass = klass
    end

    def to_sql
      "SELECT * FROM #{@klass.table_name}"
    end

    def records
      @records ||= @klass.find_by_sql(to_sql)
    end
  end
end

在 method 加上

# mavericks/lib/mavericks/data_record/method.rb

def all
  Relation.new(self).records
end

def last
  all.last
end

def find(id)
  find_by_sql("SELECT * FROM #{self.table_name} WHERE id = #{id.to_i}").first
end

def find_by_sql(sql)
  connection.execute(sql).map do |attributes|
    new(attributes)
  end
end

這裡我們雖然是呼叫 Task.all,但其實背地裡是呼叫 Relation 的 records,等於用 Relation 來包裝起來

記得加上 relation.rb

# mavericks/lib/mavericks/data_record/relation.rb

require 'mavericks/support'
require_relative "./data_record/relation"
require_relative "./data_record/connection_adapter.rb"
require_relative "./data_record/persistence"
require_relative "./data_record/method"
require_relative "./data_record/base"

module Mavericks
  module DataRecord
  end
end

這時候回到 sqlite_test.rb 印出來會是一個一個物件

# just_do/sqlite_test.rb

require 'mavericks/data_record'

Mavericks::DataRecord::Base.establish_connection

class Task < Mavericks::DataRecord::Base
end

puts Task.all

關於那個 chain

可是為什麼要這樣包呢?多那一層的 Relation 有什麼好處?還記得 Rails 的查詢 where 嗎?

他可以這樣用 Task.where(...).where(...),所以為了達到這樣的效果,我們可以這樣實作

# mavericks/lib/mavericks/data_record/method.rb

def where(query)
  sql_syntax = query.map do |key, val|
    "#{key.to_s} = #{self.to_sql(val)}"
  end
  Relation.new(self).where(sql_syntax)
end

我們一樣在寫一個 class method,讓 Task 可以這樣呼叫 Task.where(...),但實際上我們是 new 一個 Relation 的物件,並且將 where 包含的查詢語法傳遞下去,那在 Relation 那邊會怎麼處理呢?

我們來修改一下 relation.rb 的程式碼

# mavericks/lib/mavericks/data_record/relation.rb

module Mavericks
  class Relation
    def initialize(klass)
      @klass = klass
      @where_values = []
    end

    def to_sql
      sql = "SELECT * FROM #{@klass.table_name}"
      if @where_values.any?
        sql += " WHERE " + @where_values.join(' AND ')
      end
      sql
    end

    def to_sql_text(val)
      case val
      when Numeric
        val.to_s
      when String
        "'#{val}'"
      else
        raise "Can't support #{val.class} to SQL!"
      end
    end

    def records
      @records ||= @klass.find_by_sql(to_sql)
    end

    def where(sql_syntax)
      if sql_syntax.class == Hash
        @where_values += sql_syntax.map { |key, val| "#{key.to_s} = #{self.to_sql_text(val)}" }
      else
        @where_values += [sql_syntax]
      end
      self
    end

    def first
      records.first
    end

    def each(&block)
      records.each(&block)
    end
  end
end

看一下 where 裡面實作的過程,可以看到我們其實也是用字串吧 where 的查詢語法拼起來,有幾個就拼幾個,這樣就可以一直串下去,一樣用 sqlite_test.rb 來試試看吧

# just_do/sqlite_test.rb

require 'mavericks/data_record'

Mavericks::DataRecord::Base.establish_connection

class Task < Mavericks::DataRecord::Base
end

Task.where(title: '鐵人30').where(content: '每天寫一篇').each do |task|
  puts task.title
end

上一篇
[DAY 18] 復刻 Rails - class_eval
下一篇
[DAY 20] 復刻 Rails - 用 Rails 的方式整理程式碼 Active Record
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言